﻿using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using System.Diagnostics;

namespace BurningSphere.Simulator
{
    static class TexturedMesh
    {
        /// <summary>
        /// Just like Mesh.Box but returns a mesh with a single set of texture coordinates.
        /// This overload uses the default box texture which is one texture per face
        /// </summary>
        /// <param name="device">The device to create the mesh on</param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        public static Mesh Box(Device device, float width, float height, float depth)
        {
            return Box(device, width, height, depth, TextureTechnique.PerFace);
        }

        /// <summary>
        /// Just like Mesh.Box but returns a mesh with a single set of texture coordinates
        /// </summary>
        /// <param name="device">The device to create the mesh on</param>
        /// <param name="width"></param>
        /// <param name="height"></param>
        /// <param name="depth"></param>
        /// <returns></returns>
        public static Mesh Box(Device device, float width, float height, float depth, TextureTechnique technique)
        {
            Debug.Assert(device != null);

            //Ensure we dispose the temporary mesh
            using (Mesh nonTexturedMesh = Mesh.Box(device, width, height, depth))
            {
                switch (technique)
                {
                    case TextureTechnique.PerFace:
                        return SetBoxTexture(CloneAddTexture(nonTexturedMesh));

                    case TextureTechnique.Divide:
                        //How to hook this up to the calls from last time?
                        throw new NotImplementedException();

                    case TextureTechnique.Spherical:
                        return SetSphericalTexture(CloneAddTexture(nonTexturedMesh),false);

                    default:
                        throw new NotImplementedException("Bad TextureTechnique");
                }
            }
        }


        /// <summary>
        /// Just like Mesh.Teapot but returns a mesh with a single set of texture coordinates
        /// The texture coordinates default to a spherically mapped set.
        /// </summary>
        /// <param name="device">The device to create the mesh on</param>
        /// <returns></returns>
        public static Mesh Teapot(Device device)
        {
            Debug.Assert(device != null);

            //Ensure we dispose the temporary mesh
            using (Mesh nonTexturedMesh = Mesh.Teapot(device))
            {
				return SetSphericalTexture(CloneAddTexture(nonTexturedMesh), false);
            }
        }

        /// <summary>
        /// Just like Mesh.Torus but returns a mesh with a single set of texture coordinates
        /// The texture coordinates default to a spherically mapped set.
        /// </summary>
        /// <param name="device"></param>
        /// <param name="innerRadius"></param>
        /// <param name="outerRadius"></param>
        /// <param name="sides"></param>
        /// <param name="rings"></param>
        /// <returns></returns>
        public static Mesh Torus(Device device, float innerRadius, float outerRadius, int sides, int rings)
        {
            Debug.Assert(device != null);

            //Ensure we dispose the temporary mesh
            using (Mesh nonTexturedMesh = Mesh.Torus(device, innerRadius, outerRadius, sides, rings))
            {
				return SetSphericalTexture(CloneAddTexture(nonTexturedMesh), false);
            }
        }

        /// <summary>
        /// Just like Mesh.Sphere but returns a mesh with a single set of texture coordinates
        /// The texture coordinates default to a spherically mapped set.
        /// </summary>
        /// <param name="device"></param>
        /// <param name="radius"></param>
        /// <param name="slices"></param>
        /// <param name="stacks"></param>
        /// <returns></returns>
        public static Mesh Sphere(Device device, float radius, int slices, int stacks)
        {
			return Sphere(device, radius, slices, stacks, false);
        }

		/// <summary>
		/// Just like Mesh.Sphere but returns a mesh with a single set of texture coordinates
		/// The texture coordinates default to a spherically mapped set.
		/// </summary>
		/// <param name="device"></param>
		/// <param name="radius"></param>
		/// <param name="slices"></param>
		/// <param name="stacks"></param>
		/// <returns></returns>
		public static Mesh Sphere(Device device, float radius, int slices, int stacks, bool invertNormals)
		{
			Debug.Assert(device != null);

			//Ensure we dispose the temporary mesh
			using (Mesh nonTexturedMesh = Mesh.Sphere(device, radius, slices, stacks))
			{
				return SetSphericalTexture(CloneAddTexture(nonTexturedMesh), invertNormals);
			}
		}

        /// <summary>
        /// Just like Mesh.Cylinder but returns a mesh with a single set of texture coordinates
        /// The texture coordinates default to a spherically mapped set.
        /// </summary>
        /// <param name="device"></param>
        /// <param name="radius1"></param>
        /// <param name="radius2"></param>
        /// <param name="length"></param>
        /// <param name="slices"></param>
        /// <param name="stacks"></param>
        /// <returns></returns>
        public static Mesh Cylinder(Device device, float radius1, float radius2, float length, int slices, int stacks)
        {
            Debug.Assert(device != null);

            //Ensure we dispose the temporary mesh
            using (Mesh nonTexturedMesh = Mesh.Cylinder(device, radius1, radius2, length, slices, stacks))
            {
				return SetSphericalTexture(CloneAddTexture(nonTexturedMesh), false);
            }
        }

        /// <summary>
        /// clones the mesh, adding texture coordinates.
        /// </summary>
        /// <param name="mesh"></param>
        /// <returns></returns>
        private static Mesh CloneAddTexture(Mesh mesh)
        {
            Debug.Assert((mesh.VertexFormat & VertexFormats.Texture1) == 0, "cloneAddTexture called with mesh that already contains texture coordinates");
            return mesh.Clone(mesh.Options.Value, mesh.VertexFormat | VertexFormats.Texture1, mesh.Device);
        }

        /// <summary>
        /// Fills in a set of spherically mapped texture coordinates to the passed in mesh
        /// </summary>
        /// <param name="mesh">The mesh to add set the texture coordinates. This mesh must have the Texture1 custom vertex availalbe</param>
        /// <returns>The mesh passed in with texture coordinates set</returns>
		public static Mesh SetSphericalTexture(Mesh mesh, bool invertNormals)
        {
            Vector3 vertexRay;
            Vector3 meshCenter;
            double phi;
            float u;

			Vector3 north = new Vector3(0f, 1f, 0f);
			Vector3 equator = new Vector3(1f, 0f, 0f);
			Vector3 northEquatorCross = Vector3.Cross(north, equator);

            MeshHelper.ComputeBoundingSphere(mesh, out meshCenter);

            using (VertexBuffer vb = mesh.VertexBuffer)
            {
                CustomVertex.PositionNormalTextured[] verts = (CustomVertex.PositionNormalTextured[])vb.Lock(0, typeof(CustomVertex.PositionNormalTextured), LockFlags.None, mesh.NumberVertices);
                try
                {
                    for (int i = 0; i < verts.Length; i++)
                    {
                        //For each vertex take a ray from the centre of the mesh to the vertex 
                        //and normalize so the dot products work.
                        vertexRay = Vector3.Normalize(verts[i].Position - meshCenter);

                        ////Ref:http://www.cs.unc.edu/~rademach/xroads-RT/RTarticle.html and
                        ////Glassner, A. (ed) An Introduction to Ray Tracing. Academic Press New York, N.Y. 1989. 
                        //phi = Math.Acos((double)Vector3.Dot(north, vertexRay));
                        //verts[i].Tv = (float)(phi / Math.PI);
                        //if (phi == 0.0) //if north and vertex ray are coincident then we can pick an arbitray u since its the entire top/bottom line of the texture
                        //{
                        //    u = 0.5f;
                        //}
                        //else
                        //{
                        //    //Clamp the acos() param to 1.0/-1.0 (rounding errors are sometimes taking it slightly over.
                        //    u = (float)(Math.Acos(Math.Max(Math.Min((double)Vector3.Dot(equator, vertexRay) / Math.Sin(phi), 1.0), -1.0)) / (2.0 * Math.PI));
                        //    if (Vector3.Dot(northEquatorCross, vertexRay) < 0.0)
                        //    {
                        //        verts[i].Tu = u;
                        //    }
                        //    else
                        //    {
                        //        verts[i].Tu = 1 - u;
                        //    }
                        //}

                        //Since we know we are using normalised axis we can simplify this somewhat!
                        //Note these simplifcations only apply if the basis vectors are the unit axis
                        //north=(0,0,1)=zaxis, equator=(0,1,0)=yaxis and north x equator=(1,0,0)=xaxis
                        //since (0,0,1)dot(x,y,z)==z and (0,1,0)dot(x,y,z)==y

                        //if north and vertex ray are coincident then we can pick an arbitray u since its the entire top/bottom line of the texture
                        phi = Math.Acos((double)vertexRay.Z);
                        verts[i].Tv = (float)(phi / Math.PI);

                        if (vertexRay.Z == 1.0f || vertexRay.Z == -1.0f)
                        {
                            verts[i].Tu = 0.5f;
                        }
                        else
                        {
                            u = (float)(Math.Acos(Math.Max(Math.Min((double)vertexRay.Y / Math.Sin(phi), 1.0), -1.0)) / (2.0 * Math.PI));
                            //Since the cross product is just giving us (1,0,0) i.e. the xaxis 
                            //and the dot product was giving us a +ve or -ve angle, we can just compare the x value with 0
                            verts[i].Tu = (vertexRay.X > 0f) ? u : 1 - u;
                        }

						if (invertNormals)
						{
							verts[i].Nx = -verts[i].Nx;
							verts[i].Ny = -verts[i].Ny;
							verts[i].Nz = -verts[i].Nz;
						}

                    }
                }
                finally
                {
                    vb.Unlock();
                }
            }
            return mesh;
        }

        /// <summary>
        /// Sets the default box texture or one texture per face.
        /// </summary>
        /// <param name="box"></param>
        /// <returns></returns>
        public static Mesh SetBoxTexture(Mesh box)
        {
            return SetBoxTexture(box, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f);
        }


        /// <summary>
        /// Sets a box texture per face. Parameters vary the stretching, offset and rotation
        /// </summary>
        /// <param name="box"></param>
        /// <param name="scaleX"></param>
        /// <param name="scaleY"></param>
        /// <param name="angle"></param>
        /// <param name="offsetX"></param>
        /// <param name="offsetY"></param>
        public static Mesh SetBoxTexture(Mesh box, float scaleX, float scaleY, float angle, float offsetX, float offsetY)
        {
            //Check the mesh looks like a box and has texture coordinates
            Debug.Assert(box.NumberVertices == 24 && box.NumberFaces == 12, "SetBoxTexture called with a mesh which does not look like a box");
            Debug.Assert((box.VertexFormat & VertexFormats.Texture1) != 0, "SetBoxTexture called with mesh with no Texture coordinates");

            Matrix transform =
                Matrix.Translation(offsetX, offsetY, 0f)
                * Matrix.Translation(-0.5f, -0.5f, 0f)     //Translate the center of rotation
                * Matrix.RotationZ(angle)
                * Matrix.Scaling(1 / scaleX, 1 / scaleY, 1f)
                * Matrix.Translation(.5f, .5f, 0f);        //and move back the center of rotation

            //The following code makes the assumption that the vertices of the box are generated the same way
            //as they are in the April 2005 SDK
            using (VertexBuffer vb = box.VertexBuffer)
            {
                CustomVertex.PositionNormalTextured[] verts = (CustomVertex.PositionNormalTextured[])vb.Lock(0, typeof(CustomVertex.PositionNormalTextured), LockFlags.None, box.NumberVertices);
                try
                {
                    for (int i = 0; i < verts.Length; i += 4)
                    {
                        //Start with the unit square where the uv coordinates cover the entire texture
                        Vector2[] corners = new Vector2[4] 
                            {
                            new Vector2(0f, 0f), //bottom left
                            new Vector2(1f, 0f), //top left
                            new Vector2(1f, 1f), //top right
                            new Vector2(0f, 1f)  //bottom right
                            };


                        //apply the transformation to each corner and store the result back into the texture coordinates
                        for (int j = 0; j < 4; j++)
                        {
                            corners[j].TransformCoordinate(transform);
                            verts[i + j].Tu = corners[j].X;
                            verts[i + j].Tv = corners[j].Y;
                        }
                    }
                }
                finally
                {
                    vb.Unlock();
                }
            }
            return box;
        }

        /// <summary>
        /// Sets a box texture where a 3x2 grid of face textures is split one per face
        /// </summary>
        /// <param name="box"></param>
        /// <param name="technique"></param>
        /// <returns></returns>
        public static Mesh SetBoxTexture(Mesh box, TextureTechnique technique)
        {
            //So far we only have one technique so we will ignore that parameter

            //Check the mesh looks like a box and has texture coordinates
            Debug.Assert(box.NumberVertices == 24 && box.NumberFaces == 12, "SetBoxTexture called with a mesh which does not look like a box");
            Debug.Assert((box.VertexFormat & VertexFormats.Texture1) != 0, "SetBoxTexture called with mesh with no Texture coordinates");

            //The following code makes the assumption that the vertices of the box are generated the same way
            //as they are in the April 2005 SDK
            using (VertexBuffer vb = box.VertexBuffer)
            {
                CustomVertex.PositionNormalTextured[] verts = (CustomVertex.PositionNormalTextured[])vb.Lock(0, typeof(CustomVertex.PositionNormalTextured), LockFlags.None, box.NumberVertices);
                try
                {
                    //Assume the texture for this box is 3 horizontal by 2 vertical faces
                    //In this order - left to right, top to bottom
                    //bottom, top, back
                    //left, front, right

                    //Bottom
                    verts[13].Tu = 0f; verts[13].Tv = 0f;
                    verts[14].Tu = 1f / 3f; verts[14].Tv = 0f;
                    verts[15].Tu = 1f / 3f; verts[15].Tv = 0.5f;
                    verts[12].Tu = 0f; verts[12].Tv = 0.5f;

                    //Top
                    verts[5].Tu = 1f / 3f; verts[5].Tv = 0f;
                    verts[6].Tu = 2f / 3f; verts[6].Tv = 0f;
                    verts[7].Tu = 2f / 3f; verts[7].Tv = 0.5f;
                    verts[4].Tu = 1f / 3f; verts[4].Tv = 0.5f;

                    //Back
                    verts[18].Tu = 2f / 3f; verts[18].Tv = 0f;
                    verts[19].Tu = 1f; verts[19].Tv = 0f;
                    verts[16].Tu = 1f; verts[16].Tv = 0.5f;
                    verts[17].Tu = 2f / 3f; verts[17].Tv = 0.5f;

                    //Left
                    verts[2].Tu = 0f; verts[2].Tv = 0.5f;
                    verts[3].Tu = 1f / 3f; verts[3].Tv = 0.5f;
                    verts[0].Tu = 1f / 3f; verts[0].Tv = 1f;
                    verts[1].Tu = 0f; verts[1].Tv = 1f;

                    //Front
                    verts[21].Tu = 1f / 3f; verts[21].Tv = 0.5f;
                    verts[22].Tu = 2f / 3f; verts[22].Tv = 0.5f;
                    verts[23].Tu = 2f / 3f; verts[23].Tv = 1f;
                    verts[20].Tu = 1f / 3f; verts[20].Tv = 1f;

                    //Right
                    verts[8].Tu = 2f / 3f; verts[8].Tv = 0.5f;
                    verts[9].Tu = 1f; verts[9].Tv = 0.5f;
                    verts[10].Tu = 1f; verts[10].Tv = 1f;
                    verts[11].Tu = 2f / 3f; verts[11].Tv = 1f;

                }
                finally
                {
                    vb.Unlock();
                }
            }
            return box;

        }


    }

    /// <summary>
    /// Used to indicate which texture technique should be used when texturing a mesh
    /// </summary>
    public enum TextureTechnique
    {
        PerFace,
        Divide,
        Spherical
    }

    /// <summary>
    /// The types of standard mesh that this library understand.
    /// </summary>
    public enum MeshType
    {
        Box,
        Sphere,
        Cylinder,
        Torus,
        Teapot
    }
}
